/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│ vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8                               :vi │
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2020 Justine Alexandra Roberts Tunney                              │
│                                                                              │
│ Permission to use, copy, modify, and/or distribute this software for         │
│ any purpose with or without fee is hereby granted, provided that the         │
│ above copyright notice and this permission notice appear in all copies.      │
│                                                                              │
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL                │
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED                │
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE             │
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL         │
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR        │
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER               │
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR             │
│ PERFORMANCE OF THIS SOFTWARE.                                                │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "dsp/audio/cosmoaudio/cosmoaudio.h"
#include "dsp/core/core.h"
#include "dsp/core/half.h"
#include "dsp/core/illumination.h"
#include "dsp/mpeg/pl_mpeg.h"
#include "dsp/scale/scale.h"
#include "dsp/tty/quant.h"
#include "dsp/tty/tty.h"
#include "libc/assert.h"
#include "libc/calls/calls.h"
#include "libc/calls/internal.h"
#include "libc/calls/struct/framebufferfixedscreeninfo.h"
#include "libc/calls/struct/framebuffervirtualscreeninfo.h"
#include "libc/calls/struct/iovec.h"
#include "libc/calls/struct/itimerval.h"
#include "libc/calls/struct/sigaction.h"
#include "libc/calls/struct/siginfo.h"
#include "libc/calls/struct/sigset.h"
#include "libc/calls/struct/stat.h"
#include "libc/calls/struct/timespec.h"
#include "libc/calls/struct/winsize.h"
#include "libc/calls/termios.h"
#include "libc/calls/ucontext.h"
#include "libc/ctype.h"
#include "libc/cxxabi.h"
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/fmt/conv.h"
#include "libc/fmt/itoa.h"
#include "libc/intrin/kprintf.h"
#include "libc/intrin/safemacros.h"
#include "libc/intrin/xchg.h"
#include "libc/log/check.h"
#include "libc/log/log.h"
#include "libc/macros.h"
#include "libc/math.h"
#include "libc/mem/alg.h"
#include "libc/mem/mem.h"
#include "libc/nexgen32e/bench.h"
#include "libc/nexgen32e/x86feature.h"
#include "libc/nt/console.h"
#include "libc/nt/enum/threadpriority.h"
#include "libc/nt/runtime.h"
#include "libc/nt/thread.h"
#include "libc/runtime/runtime.h"
#include "libc/sock/sock.h"
#include "libc/sock/struct/pollfd.h"
#include "libc/stdio/internal.h"
#include "libc/stdio/rand.h"
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/str/strwidth.h"
#include "libc/str/unicode.h"
#include "libc/sysv/consts/af.h"
#include "libc/sysv/consts/auxv.h"
#include "libc/sysv/consts/clock.h"
#include "libc/sysv/consts/ex.h"
#include "libc/sysv/consts/exit.h"
#include "libc/sysv/consts/f.h"
#include "libc/sysv/consts/fileno.h"
#include "libc/sysv/consts/ipproto.h"
#include "libc/sysv/consts/itimer.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/mlock.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/ok.h"
#include "libc/sysv/consts/poll.h"
#include "libc/sysv/consts/prio.h"
#include "libc/sysv/consts/prot.h"
#include "libc/sysv/consts/s.h"
#include "libc/sysv/consts/sa.h"
#include "libc/sysv/consts/shut.h"
#include "libc/sysv/consts/sig.h"
#include "libc/sysv/consts/splice.h"
#include "libc/sysv/consts/termios.h"
#include "libc/sysv/consts/w.h"
#include "libc/sysv/errfuns.h"
#include "libc/thread/thread.h"
#include "libc/time.h"
#include "libc/x/xsigaction.h"
#include "third_party/getopt/getopt.internal.h"
#include "third_party/stb/stb_image_resize.h"
#include "tool/viz/lib/graphic.h"
#include "tool/viz/lib/knobs.h"
#include "tool/viz/lib/ycbcr.h"

/**
 * @fileoverview MPEG Video Player for Terminal.
 */

#define GAMMADELTA    0.1
#define NETBUFSIZ     (2 * 1024 * 1024)
#define MAX_FRAMERATE (1 / 60.)

#define USAGE \
  " [FLAGS] MPG\n\
Renders motion picture to teletypewriters.\n\
\n\
Flags & Keyboard Shortcuts:\n\
  -s         stats\n\
  -t         true color\n\
  -d         dithering\n\
  -3         ibm cp437 rendering\n\
  -4         unicode rendering\n\
  -a         ansi quantization\n\
  -x         xterm256 quantization\n\
  -A         assume ansi ansi palette\n\
  -T         assume tango ansi palette\n\
  -v         increases verbosity        [flag]\n\
  -L PATH    redirects stderr to path   [flag]\n\
  -y         yes to interactive prompts [flag]\n\
  -h or -?   shows this information     [flag]\n\
  UP/DOWN    adjust volume              [keyboard]\n\
  CTRL+L     redraw                     [keyboard]\n\
  CTRL+Z     suspend                    [keyboard]\n\
  CTRL+C     exit                       [keyboard]\n\
  q          quit                       [keyboard]\n\
\n\
Effects Shortcuts:\n\
\n\
  S          Toggle Swing (TV, PC)\n\
  Y          Toggle Black/White Mode\n\
  p          Toggle Primaries (BT.601, BT.709)\n\
  g          +Gamma           G          -Gamma\n\
  l          +Illumination    L          -Illumination\n\
  k          +LumaKernel      K          -LumaKernel\n\
  j          +ChromaKernel    J          -ChromaKernel\n\
  CTRL-G     {Unsharp,Sharp}\n\
\n\
Environment Variables:\n\
  ROWS=𝑦     sets height [inarticulate mode]\n\
  COLUMNS=𝑥  sets width  [inarticulate mode]\n\
  TERM=dumb  inarticulate mode\n\
\n\
Notes:\n\
\n\
Your video printer natively supports .mpg files. If your videos are\n\
in a different format, then it's fast and easy to convert them:\n\
\n\
    ffmpeg -i movie.mkv movie.mpg\n\
\n\
The terminal fonts we recommend are PragmataPro, Bitstream Vera Sans\n\
Mono (known as DejaVu Sans Mono in the open source community), Menlo,\n\
and Lucida Console.\n\
\n"

#define CTRL(C)   ((C) ^ 0100)
#define ALT(C)    ((033 << 010) | (C))
#define ARGZ(...) ((char *const[]){__VA_ARGS__, NULL})
#define MOD(X, Y) ((X) - (ABS(Y)) * ((X) / ABS(Y)))

#define BALLOC(B, A, N, NAME)               \
  ({                                        \
    INFOF("balloc/%s %,zu bytes", NAME, N); \
    *(B) = pvalloc(N);                      \
  })

#define TIMEIT(OUT_NANOS, FORM)                                           \
  do {                                                                    \
    struct timespec Start = timespec_mono();                              \
    FORM;                                                                 \
    (OUT_NANOS) = timespec_tonanos(timespec_sub(timespec_mono(), Start)); \
  } while (0)

typedef bool (*openspeaker_f)(void);

enum Sharp {
  kSharpNone,
  kSharpUnsharp,
  kSharpSharp,
  kSharpMAX,
};

enum Blur {
  kBlurNone,
  kBlurBox,
  kBlurGaussian,
  kBlurMAX,
};

struct NamedVector {
  char name[8];
  const double (*vector)[3];
};

struct VtFrame {
  size_t i, n;
  union {
    void *b;
    char *bytes;
  };
};

struct FrameCountRing {
  size_t i, n;
  float p[64]; /* seconds relative to starttime_ */
};

struct FrameBuffer {
  void *map;
  size_t size;
  char *path;
  int fd;
  struct FrameBufferFixedScreenInfo fscreen;
  struct FrameBufferVirtualScreenInfo vscreen;
};

static const struct NamedVector kPrimaries[] = {
    {"BT.601", &kBt601Primaries},
    {"BT.709", &kBt709Primaries},
};

static const struct NamedVector kLightings[] = {
    {"A", &kIlluminantA},           {"C", &kIlluminantC},
    {"D50", &kIlluminantD50},       {"D55", &kIlluminantD55},
    {"D65", &kIlluminantD65},       {"D75", &kIlluminantD75},
    {"F2", &kIlluminantF2},         {"F7", &kIlluminantF7},
    {"F11", &kIlluminantF11},       {"A-10", &kIlluminantAD10},
    {"C-10", &kIlluminantCD10},     {"D50-10", &kIlluminantD50D10},
    {"D55-10", &kIlluminantD55D10}, {"D65-10", &kIlluminantD65D10},
    {"D75-10", &kIlluminantD75D10}, {"F2-10", &kIlluminantF2D10},
    {"F7-10", &kIlluminantF7D10},   {"F11-10", &kIlluminantF11D10},
};

static plm_t *plm_;
static float gamma_;
static float volscale_;
struct CosmoAudio *ca_;
static enum Blur blur_;
static enum Sharp sharp_;
static jmp_buf jb_, jbi_;
static double pary_, parx_;
static struct TtyIdent ti_;
static struct YCbCr *ycbcr_;
static bool emboss_, sobel_;
static const char *patharg_;
static struct winsize wsize_;
static float hue_, sat_, lit_;
static volatile bool resized_;
static void *xtcodes_, *audio_;
static struct FrameBuffer fb0_;
static unsigned chans_, srate_;
static volatile bool ignoresigs_;
static size_t dh_, dw_, framecount_;
static struct FrameCountRing fcring_;
static int lumakernel_, chromakernel_;
static int primaries_, lighting_, swing_;
static uint64_t t1, t2, t3, t4, t5, t6, t8;
static int homerow_, lastrow_, infd_, outfd_;
static struct VtFrame vtframe_[2], *f1_, *f2_;
static struct Graphic graphic_[2], *g1_, *g2_;
static struct timespec deadline_, dura_, starttime_;
static bool yes_, stats_, dither_, ttymode_, istango_;
static struct timespec decode_start_, f1_start_, f2_start_;
static bool fullclear_, historyclear_, tuned_, yonly_, gotvideo_;
static char status_[7][200], logpath_[PATH_MAX], chansstr_[32], sratestr_[32];

static void OnCtrlC(void) {
  longjmp(jb_, 1);
}

static void OnResize(void) {
  resized_ = true;
}

static struct timespec GetGraceTime(void) {
  return timespec_sub(deadline_, timespec_mono());
}

static char *strntoupper(char *s, size_t n) {
  size_t i;
  for (i = 0; s[i] && i < n; ++i)
    if ('a' <= s[i] && s[i] <= 'z')
      s[i] -= 'a' - 'A';
  return s;
}

static int GetNamedVector(const struct NamedVector *choices, size_t n,
                          const char *s) {
  int i;
  char name[sizeof(choices->name)];
#pragma GCC push_options
#pragma GCC diagnostic ignored "-Wstringop-truncation"
  strncpy(name, s, sizeof(name));
#pragma GCC pop_options
  strntoupper(name, sizeof(name));
  for (i = 0; i < n; ++i)
    if (memcmp(choices[i].name, name, sizeof(name)) == 0)
      return i;
  return -1;
}

static int GetPrimaries(const char *s) {
  return GetNamedVector(kPrimaries, ARRAYLEN(kPrimaries), s);
}

static int GetLighting(const char *s) {
  return GetNamedVector(kLightings, ARRAYLEN(kLightings), s);
}

static void CloseSpeaker(void) {
  cosmoaudio_close(ca_);
  ca_ = 0;
}

static void ResizeVtFrame(struct VtFrame *f, size_t yn, size_t xn) {
  BALLOC(&f->b, 4096, 64 + yn * (xn * 32 + 8), __FUNCTION__);
  f->i = f->n = 0;
}

static float timespec_tofloat(struct timespec ts) {
  return ts.tv_sec + ts.tv_nsec * 1e-9;
}

static void RecordFactThatFrameWasFullyRendered(void) {
  fcring_.p[fcring_.i] =
      timespec_tofloat(timespec_sub(timespec_mono(), starttime_));
  fcring_.n += 1;
  fcring_.i += 1;
  fcring_.i &= ARRAYLEN(fcring_.p) - 1;
}

static double MeasureFrameRate(void) {
  int i, j, n, m;
  if (fcring_.n) {
    m = ARRAYLEN(fcring_.p);
    n = MIN(fcring_.n, m);
    i = (fcring_.i - 1) & (m - 1);
    j = (fcring_.i - n) & (m - 1);
    return n / (fcring_.p[i] - fcring_.p[j]);
  } else {
    return 0;
  }
}

static bool ShouldUseFrameBuffer(void) {
  return fb0_.fd != -1;
}

static bool IsHighDefinition(long yn, long xn) {
  return yn * xn >= 1280 * 720;
}

static void ComputeColoringSolution(void) {
  YCbCrInit(&ycbcr_, yonly_, swing_, gamma_, *kPrimaries[primaries_].vector,
            *kLightings[lighting_].vector);
}

static void DimensionDisplay(void) {
  size_t yn, xn;
  double ratio, height, width;
  do {
    resized_ = false;
    if (ShouldUseFrameBuffer()) {
      pary_ = 1;
      parx_ = 1;
      dh_ = fb0_.vscreen.yres;
      dw_ = fb0_.vscreen.xres;
      wsize_.ws_row = fb0_.vscreen.yres_virtual;
      wsize_.ws_col = fb0_.vscreen.xres_virtual;
    } else {
      pary_ = 1;
      parx_ = 1;
      wsize_.ws_row = 25;
      wsize_.ws_col = 80;
      wsize_ = (struct winsize){.ws_row = 40, .ws_col = 80};
      if (tcgetwinsize(outfd_, &wsize_) == -1)
        tcgetwinsize(0, &wsize_);
      dh_ = wsize_.ws_row * 2;
      dw_ = wsize_.ws_col * 2;
    }
    ratio = g1_->xn;
    ratio /= g1_->yn;
    height = dh_;
    width = dw_;
    height = MIN(height, height * ratio);
    width = MIN(width, width * ratio);
    yn = height;
    xn = width;
    yn = ROUNDDOWN(yn, 2);
    xn = ROUNDDOWN(xn, 2);
    g2_ = resizegraphic(&graphic_[1], yn, xn);
    INFOF("%s 𝑑(%hu×%hu)×(%d,%d): 𝑔₁(%zu×%zu,r=%f) → 𝑔₂(%zu×%zu)",
          "DimensionDisplay", wsize_.ws_row, wsize_.ws_col, g1_->yn, g1_->xn,
          ratio, yn, xn);
    BALLOC(&xtcodes_, 64, ((g2_->yn) * g2_->xn + 8) * sizeof(struct TtyRgb),
           "xtcodes_");
    ResizeVtFrame(&vtframe_[0], (g2_->yn), g2_->xn);
    ResizeVtFrame(&vtframe_[1], (g2_->yn), g2_->xn);
    f1_ = &vtframe_[0];
    f2_ = &vtframe_[1];
    if (ttymode_)
      homerow_ = MIN(wsize_.ws_row - HALF(g2_->yn),
                     HALF(wsize_.ws_row - HALF(g2_->yn)));
    lastrow_ = homerow_ + HALF(g2_->yn);
    ComputeColoringSolution();
  } while (resized_);
}

static bool OpenSpeaker(void) {
  struct CosmoAudioOpenOptions cao = {};
  cao.sizeofThis = sizeof(struct CosmoAudioOpenOptions);
  cao.deviceType = kCosmoAudioDeviceTypePlayback;
  cao.sampleRate = srate_;
  cao.channels = chans_;
  return cosmoaudio_open(&ca_, &cao) == COSMOAUDIO_SUCCESS;
}

static void OnAudio(plm_t *mpeg, plm_samples_t *samples, void *user) {
  if (!ca_)
    return;
  if (volscale_ != 1.f)
    for (unsigned i = 0; i < samples->count * chans_; ++i)
      samples->interleaved[i] *= volscale_;
  cosmoaudio_write(ca_, samples->interleaved, samples->count);
}

static void DescribeAlgorithms(char *p) {
  if (dither_ && TTYQUANT()->alg != kTtyQuantTrue)
    p = stpcpy(p, " ℍithered");
  if (yonly_)
    p = stpcpy(p, " grayscaled");
  p += sprintf(p, " magikarp:%d:%d", lumakernel_, chromakernel_);
  switch (TTYQUANT()->alg) {
    case kTtyQuantTrue:
      p = stpcpy(p, " true-color");
      break;
    case kTtyQuantXterm256:
      p = stpcpy(p, " xterm256");
      break;
    case kTtyQuantAnsi:
      p = stpcpy(p, " aixterm ansi");
      if (istango_)
        p = stpcpy(p, " tango");
      break;
    default:
      break;
  }
  switch (TTYQUANT()->blocks) {
    case kTtyBlocksCp437:
      p = stpcpy(p, " ibm cp437");
      break;
    case kTtyBlocksUnicode:
      p = stpcpy(p, " unicode");
      break;
    default:
      break;
  }
  *p++ = ' ';
  *p = '\0';
}

static char *StartRender(char *vt) {
  if (!ttymode_)
    vt += sprintf(vt, "\r\n\r\n");
  if (fullclear_) {
    vt += sprintf(vt, "\e[0m\e[H\e[J");
    fullclear_ = false;
  } else if (historyclear_) {
    vt += sprintf(vt, "\e[0m\e[H\e[J\e[3J");
    historyclear_ = false;
  }
  vt += sprintf(vt, "\e[%hhuH", homerow_ + 1);
  return vt;
}

static void EndRender(char *vt) {
  vt += sprintf(vt, "\e[0m");
  f2_->n = (intptr_t)vt - (intptr_t)f2_->b;
  f2_->i = 0;
}

static bool IsNonZeroFloat(float f) {
  return fabsf(f) > 0.001f;
}

static bool HasAdjustments(void) {
  return (IsNonZeroFloat(hue_) || IsNonZeroFloat(sat_) ||
          IsNonZeroFloat(lit_)) ||
         (emboss_ || sharp_ || blur_ || sobel_ || pf1_ || pf2_ || pf3_ ||
          pf4_ || pf5_ || pf6_ || pf7_ || pf8_ || pf9_ || pf10_ || pf11_ ||
          pf12_);
}

static char *DescribeAdjustments(char *p) {
  if (emboss_)
    p = stpcpy(p, " emboss");
  if (sobel_)
    p = stpcpy(p, " sobel");
  switch (sharp_) {
    case kSharpSharp:
      p = stpcpy(p, " sharp");
      break;
    case kSharpUnsharp:
      p = stpcpy(p, " unsharp");
      break;
    default:
      break;
  }
  switch (blur_) {
    case kBlurBox:
      p = stpcpy(p, " boxblur");
      break;
    case kBlurGaussian:
      p = stpcpy(p, " gaussian");
      break;
    default:
      break;
  }
  if (IsNonZeroFloat(hue_))
    p += sprintf(p, " hue%+.2f", hue_);
  if (IsNonZeroFloat(sat_))
    p += sprintf(p, " sat%+.2f", sat_);
  if (IsNonZeroFloat(lit_))
    p += sprintf(p, " lit%+.2f", lit_);
  if (pf1_)
    p = stpcpy(p, " PF1");
  if (pf2_)
    p = stpcpy(p, " PF2");
  if (pf3_)
    p = stpcpy(p, " PF3");
  if (pf4_)
    p = stpcpy(p, " PF4");
  if (pf5_)
    p = stpcpy(p, " PF5");
  if (pf6_)
    p = stpcpy(p, " PF6");
  if (pf7_)
    p = stpcpy(p, " PF7");
  if (pf8_)
    p = stpcpy(p, " PF8");
  if (pf9_)
    p = stpcpy(p, " PF9");
  if (pf10_)
    p = stpcpy(p, " PF10");
  if (pf11_)
    p = stpcpy(p, " PF11");
  if (pf12_)
    p = stpcpy(p, " PF12");
  *p++ = ' ';
  *p++ = '\0';
  return p;
}

static const char *DescribeSwing(int swing) {
  switch (swing) {
    case 219:
      return "TV";
    case 255:
      return "PC";
    default:
      return "??";
  }
}

static void RenderIt(void) {
  long bpf;
  double bpc;
  char *vt, *p;
  unsigned yn, xn;
  struct TtyRgb bg, fg;
  yn = g2_->yn;
  xn = g2_->xn;
  vt = f2_->b;
  p = StartRender(vt);
  if (TTYQUANT()->alg == kTtyQuantTrue) {
    bg = (struct TtyRgb){0, 0, 0, 0};
    fg = (struct TtyRgb){0xee, 0xff, 0xff, 0};
    p = stpcpy(p, "\e[48;2;0;0;0;38;2;255;255;255m");
  } else if (TTYQUANT()->alg == kTtyQuantAnsi) {
    bg = g_ansi2rgb_[0];
    fg = g_ansi2rgb_[7];
    p +=
        sprintf(p, "\e[%d;%dm", 30 + g_ansi2rgb_[0].xt, 40 + g_ansi2rgb_[7].xt);
  } else {
    bg = (struct TtyRgb){0, 0, 0, 16};
    fg = (struct TtyRgb){0xff, 0xff, 0xff, 231};
    p = stpcpy(p, "\e[48;5;16;38;5;231m");
  }
  p = ttyraster(p, xtcodes_, yn, xn, bg, fg);
  if (ttymode_ && stats_) {
    bpc = bpf = p - vt;
    bpc /= wsize_.ws_row * wsize_.ws_col;
    sprintf(status_[4], " %s/%s/%s %d×%d → %u×%u pixels ",
            kPrimaries[primaries_].name, DescribeSwing(swing_),
            kLightings[lighting_].name, plm_get_width(plm_),
            plm_get_height(plm_), g2_->xn, g2_->yn);
    sprintf(status_[5], " decode:%,8luµs | magikarp:%,8luµs ",
            plmpegdecode_latency_, magikarp_latency_);
    sprintf(status_[1], " ycbcr2rgb:%,8luµs | gyarados:%,8luµs ",
            ycbcr2rgb_latency_, gyarados_latency_);
    sprintf(status_[0], " fx:%,ldµs %.6fbpc %,ldbpf %.6ffps ",
            lroundl(t6 / 1e3L), bpc, bpf, (size_t)(p - vt), MeasureFrameRate());
    sprintf(status_[2], " gamma:%.1f %hu columns × %hu lines of text ", gamma_,
            wsize_.ws_col, wsize_.ws_row);
    DescribeAlgorithms(status_[3]);
    p += sprintf(p, "\e[0m");
    if (HasAdjustments()) {
      DescribeAdjustments(status_[6]);
      p += sprintf(p, "\e[%d;%dH%s", lastrow_ - 7,
                   HALF(xn) - strwidth(status_[6], 0), status_[6]);
    }
    p += sprintf(p, "\e[%d;%dH%s", lastrow_ - 6,
                 HALF(xn) - strwidth(status_[4], 0), status_[4]);
    p += sprintf(p, "\e[%d;%dH%s", lastrow_ - 5,
                 HALF(xn) - strwidth(status_[5], 0), status_[5]);
    p += sprintf(p, "\e[%d;%dH%s", lastrow_ - 4,
                 HALF(xn) - strwidth(status_[1], 0), status_[1]);
    p += sprintf(p, "\e[%d;%dH%s", lastrow_ - 3,
                 HALF(xn) - strwidth(status_[0], 0), status_[0]);
    p += sprintf(p, "\e[%d;%dH%30s", lastrow_ - 2,
                 HALF(xn) - strwidth(status_[2], 0), status_[2]);
    p += sprintf(p, "\e[%d;%dH%s", lastrow_ - 1,
                 HALF(xn) - strwidth(status_[3], 0), status_[3]);
    p += sprintf(p, "\e[%d;%dH %s %s ", lastrow_ - 2, 2,
                 program_invocation_name, "");
    p += sprintf(p, "\e[%d;%dH %s ", lastrow_ - 1, 2,
                 "by justine tunney <jtunney@gmail.com>");
  }
  EndRender(p);
}

static void RasterIt(void) {
  static bool once;
  static void *buf;
  if (!once) {
    buf = _mapanon(ROUNDUP(fb0_.size, getgransize()));
    once = true;
  }
  WriteToFrameBuffer(fb0_.vscreen.yres_virtual, fb0_.vscreen.xres_virtual, buf,
                     g2_->yn, g2_->xn, g2_->b, fb0_.vscreen.yres,
                     fb0_.vscreen.xres);
  memcpy(fb0_.map, buf, fb0_.size);
}

static void TranscodeVideo(plm_frame_t *pf) {
  CHECK_EQ(pf->cb.width, pf->cr.width);
  CHECK_EQ(pf->cb.height, pf->cr.height);
  DEBUGF("TranscodeVideo() [grace=%,ldns]", timespec_tonanos(GetGraceTime()));
  g2_ = &graphic_[1];
  t5 = 0;

  TIMEIT(t1, {
    pary_ = 2;
    if (pf1_)
      pary_ = 1.;
    if (pf2_)
      pary_ = (266 / 64.) * (900 / 1600.);
    pary_ *= plm_get_pixel_aspect_ratio(plm_);
    YCbCr2RgbScale(g2_->yn, g2_->xn, g2_->b, pf->y.height, pf->y.width,
                   (void *)pf->y.data, pf->cr.height, pf->cr.width,
                   (void *)pf->cb.data, (void *)pf->cr.data, pf->y.height,
                   pf->y.width, pf->cr.height, pf->cr.width, pf->height,
                   pf->width, pary_, parx_, &ycbcr_);
  });
  t2 = 0;
  t8 = 0;

  TIMEIT(t6, {
    switch (blur_) {
      case kBlurBox:
        boxblur(g2_);
        break;
      case kBlurGaussian:
        gaussian(g2_->yn, g2_->xn, g2_->b);
        break;
      default:
        break;
    }
    if (sobel_)
      sobel(g2_);
    if (emboss_)
      emboss(g2_);
    switch (sharp_) {
      case kSharpSharp:
        sharpen(3, g2_->yn, g2_->xn, g2_->b, g2_->yn, g2_->xn);
        break;
      case kSharpUnsharp:
        unsharp(3, g2_->yn, g2_->xn, g2_->b, g2_->yn, g2_->xn);
        break;
      default:
        break;
    }
    if (dither_ && TTYQUANT()->alg != kTtyQuantTrue)
      dither(g2_->yn, g2_->xn, g2_->b, g2_->yn, g2_->xn);
  });

  if (ShouldUseFrameBuffer()) {
    t3 = 0;
    TIMEIT(t4, RasterIt());
  } else {
    TIMEIT(t3, getxtermcodes(xtcodes_, g2_));
    TIMEIT(t4, RenderIt());
  }

  INFOF("𝑓%zu(%u×%u) %,zub (%f BPP) "
        "ycbcr=%,zuns "
        "scale=%,zuns "
        "lace=%,zuns "
        "fx=%,zuns "
        "quantize=%,zuns "
        "render=%,zuns",
        framecount_++, g2_->yn, g2_->xn, f2_->n,
        (f1_->n / (double)(g2_->yn * g2_->xn)), t1, t2, t8, t6, t3, t4);
}

static void OnVideo(plm_t *mpeg, plm_frame_t *pf, void *user) {
  gotvideo_ = true;
  if (f2_->n) {
    WARNF("video frame dropped");
  } else {
    TranscodeVideo(pf);
    if (!f1_->n) {
      struct VtFrame *t = f1_;
      f1_ = f2_, f2_ = t;
      f1_start_ = decode_start_;
    } else {
      f2_start_ = decode_start_;
    }
  }
}

static void OpenVideo(void) {
  size_t yn, xn;
  INFOF("%s(%`'s)", "OpenVideo", patharg_);
  CHECK_NOTNULL((plm_ = plm_create_with_filename(patharg_)));
  swing_ = 219;
  xn = plm_get_width(plm_);
  yn = plm_get_height(plm_);
  lighting_ = GetLighting("D65");
  primaries_ = IsHighDefinition(yn, xn) ? GetPrimaries("BT.709")
                                        : GetPrimaries("BT.601");
  plm_set_video_decode_callback(plm_, OnVideo, NULL);
  plm_set_audio_decode_callback(plm_, OnAudio, NULL);
  plm_set_loop(plm_, false);
  FormatInt64(chansstr_, (chans_ = 2));
  FormatInt64(sratestr_, (srate_ = plm_get_samplerate(plm_)));
  if (plm_get_num_audio_streams(plm_) && OpenSpeaker()) {
    plm_set_audio_enabled(plm_, true);
  } else {
    plm_set_audio_enabled(plm_, false);
  }
  g2_ = g1_ = resizegraphic(&graphic_[0], yn, xn);
}

static ssize_t WriteVideoCall(void) {
  size_t amt;
  ssize_t rc;
  amt = min(4096 * 4, f1_->n - f1_->i);
  if ((rc = write(outfd_, f1_->bytes + f1_->i, amt)) != -1) {
    if ((f1_->i += rc) == f1_->n) {
      if (plm_get_audio_enabled(plm_))
        plm_set_audio_lead_time(
            plm_,
            max(0,
                min(timespec_tofloat(timespec_sub(timespec_mono(), f1_start_)),
                    plm_get_samplerate(plm_) / PLM_AUDIO_SAMPLES_PER_FRAME)));
      f1_start_ = f2_start_;
      f1_->i = f1_->n = 0;
      struct VtFrame *t = f1_;
      f1_ = f2_, f2_ = t;
      RecordFactThatFrameWasFullyRendered();
    }
  }
  return rc;
}

static void DrainVideo(void) {
  if (f1_ && f1_->n) {
    ttywrite(outfd_, f1_->bytes + f1_->i, f1_->n - f1_->i);
    f1_->i = f1_->n = 0;
  }
  if (f2_ && f2_->n)
    f2_->i = f2_->n = 0;
}

static void WriteVideo(void) {
  ssize_t rc;
  DEBUGF("write(tty) grace=%,ldns", timespec_tonanos(GetGraceTime()));
  if ((rc = WriteVideoCall()) != -1) {
    DEBUGF("write(tty) → %zd [grace=%,ldns]", rc,
           timespec_tonanos(GetGraceTime()));
  } else if (errno == EAGAIN || errno == EINTR) {
    DEBUGF("write(tty) → EINTR");
    longjmp(jbi_, 1);
  } else if (errno == EPIPE) {
    DEBUGF("write(tty) → EPIPE");
    longjmp(jb_, 1);
  } else {
    FATALF("write(tty) → %s", strerror(errno));
  }
}

static void RefreshDisplay(void) {
  if (f1_ && f1_->n)
    f1_->i = 0;
  DimensionDisplay();
  resized_ = false;
  historyclear_ = true;
  ttysend(outfd_, "\e[0m\e[H\e[3J");
}

static void SetQuant(enum TtyQuantizationAlgorithm alg,
                     enum TtyQuantizationChannels chans,
                     enum TtyBlocksSelection blocks) {
  tuned_ = true;
  ttyquantsetup(alg, chans, blocks);
}

static void SetQuantizationAlgorithm(enum TtyQuantizationAlgorithm alg) {
  SetQuant(alg, TTYQUANT()->chans, TTYQUANT()->blocks);
  /* TODO(jart): autotune */
}

static void SetDithering(bool dither) {
  tuned_ = true;
  dither_ = dither;
}

static optimizesize bool ProcessOptKey(int opt) {
  switch (opt) {
    case 's':
      stats_ = !stats_;
      return true;
    case '3':
      TTYQUANT()->blocks = kTtyBlocksCp437;
      return true;
    case '4':
      TTYQUANT()->blocks = kTtyBlocksUnicode;
      return true;
    case 'd':
      SetDithering(!dither_);
      return true;
    case 't':
      SetQuantizationAlgorithm(kTtyQuantTrue);
      return true;
    case 'a':
      SetQuantizationAlgorithm(kTtyQuantAnsi);
      return true;
    case 'x':
      SetQuantizationAlgorithm(kTtyQuantXterm256);
      return true;
    case 'A':
      istango_ = false;
      memcpy(g_ansi2rgb_, &kCgaPalette, sizeof(kCgaPalette));
      return true;
    case 'T':
      istango_ = true;
      memcpy(g_ansi2rgb_, &kTangoPalette, sizeof(kTangoPalette));
      return true;
    default:
      return false;
  }
}

static optimizesize void ReadKeyboard(void) {
  char b[64];
  int c, i, n, sgn;
  memset(b, -1, sizeof(b));
  b[0] = CTRL('B'); /* for eof case */
  if ((n = read(infd_, &b, sizeof(b))) != -1) {
    for (;;) {
      i = 0;
      c = b[i++];
      if (!ProcessOptKey(c)) {
        sgn = isupper(c) ? -1 : 1;
        switch (c) {
          case 'Y':
            yonly_ = !yonly_;
            ComputeColoringSolution();
            break;
          case 'S':
            swing_ = swing_ == 219 ? 255 : 219;
            ComputeColoringSolution();
            break;
          case 'p':
          case 'P':
            primaries_ = MOD(sgn + primaries_, ARRAYLEN(kPrimaries));
            ComputeColoringSolution();
            break;
          case 'l':
          case 'L':
            lighting_ = MOD(sgn + lighting_, ARRAYLEN(kLightings));
            ComputeColoringSolution();
            break;
          case 'g':
          case 'G':
            gamma_ += sgn * GAMMADELTA;
            g_xterm256_gamma += sgn * GAMMADELTA;
            ComputeColoringSolution();
            break;
          case 'k':
          case 'K':
            lumakernel_ = MOD(sgn + lumakernel_, ARRAYLEN(kMagikarp));
            memcpy(g_magikarp, kMagikarp[lumakernel_], sizeof(kMagikarp[0]));
            break;
          case 'j':
          case 'J':
            chromakernel_ = MOD(sgn + chromakernel_, ARRAYLEN(kMagkern));
            memcpy(g_magkern, kMagkern[chromakernel_], sizeof(kMagkern[0]));
            break;
          case 'q':
          case CTRL('C'):
            longjmp(jb_, 1);
            break;
          case CTRL('Z'):
            ttyshowcursor(outfd_);
            raise(SIGSTOP);
            break;
          case CTRL('G'):
            sharp_ = (sharp_ + 1) % kSharpMAX;
            break;
          case CTRL('\\'):
            raise(SIGQUIT);
            break;
          case CTRL('L'):
            RefreshDisplay();
            break;
          case '\e':
            if (n == 1) {
              longjmp(jb_, 1); /* \e <𝟷𝟶𝟶ms*VTIME> is ESC */
            }
            switch (b[i++]) {
              case '[':
                switch (b[i++]) {
                  case 'A': /* "\e[A" is up arrow */
                    volscale_ *= 1.05f;
                    break;
                  case 'B': /* "\e[B" is down arrow */
                    volscale_ *= 0.95f;
                    break;
                  case 'C': /* "\e[C" is right arrow */
                    break;
                  case 'D': /* "\e[D" is left arrow */
                    break;
                  case '1':
                    switch (b[i++]) {
                      case '1':
                        switch (b[i++]) {
                          case '~': /* \e[11~ is F1 */
                            pf1_ = !pf1_;
                            break;
                          default:
                            break;
                        }
                        break;
                      case '2':
                        switch (b[i++]) {
                          case '~': /* \e[12~ is F2 */
                            pf2_ = !pf2_;
                            break;
                          default:
                            break;
                        }
                        break;
                      case '3':
                        switch (b[i++]) {
                          case '~': /* \e[13~ is F3 */
                            pf3_ = !pf3_;
                            break;
                          default:
                            break;
                        }
                        break;
                      case '4':
                        switch (b[i++]) {
                          case '~': /* \e[14~ is F4 */
                            pf4_ = !pf4_;
                            break;
                          default:
                            break;
                        }
                        break;
                      case '5':
                        switch (b[i++]) {
                          case '~': /* \e[15~ is F5 */
                            pf5_ = !pf5_;
                            break;
                          default:
                            break;
                        }
                        break;
                      case '7':
                        switch (b[i++]) {
                          case '~': /* \e[17~ is F6 */
                            pf6_ = !pf6_;
                            break;
                          default:
                            break;
                        }
                        break;
                      case '8':
                        switch (b[i++]) {
                          case '~': /* \e[18~ is F7 */
                            pf7_ = !pf7_;
                            break;
                          default:
                            break;
                        }
                        break;
                      case '9':
                        switch (b[i++]) {
                          case '~': /* \e[19~ is F8 */
                            pf8_ = !pf8_;
                            break;
                          default:
                            break;
                        }
                        break;
                      default:
                        break;
                    }
                    break;
                  case '2':
                    switch (b[i++]) {
                      case '0':
                        switch (b[i++]) {
                          case '~': /* \e[20~ is F9 */
                            pf9_ = !pf9_;
                            break;
                          default:
                            break;
                        }
                        break;
                      case '1':
                        switch (b[i++]) {
                          case '~': /* \e[21~ is F10 */
                            pf10_ = !pf10_;
                            break;
                          default:
                            break;
                        }
                        break;
                      case '3':
                        switch (b[i++]) {
                          case '~': /* \e[23~ is F11 */
                            pf11_ = !pf11_;
                            break;
                          default:
                            break;
                        }
                        break;
                      case '4':
                        switch (b[i++]) {
                          case '~': /* \e[24~ is F12 */
                            pf12_ = !pf12_;
                            break;
                          default:
                            break;
                        }
                        break;
                      default:
                        break;
                    }
                    break;
                  case '[':
                    switch (b[i++]) {
                      case 'A': /* \e[[A is F1 */
                        pf1_ = !pf1_;
                        break;
                      case 'B': /* \e[[B is F2 */
                        pf2_ = !pf2_;
                        break;
                      case 'C': /* \e[[C is F3 */
                        pf3_ = !pf3_;
                        break;
                      case 'D': /* \e[[D is F4 */
                        pf4_ = !pf4_;
                        break;
                      case 'E': /* \e[[E is F5 */
                        pf5_ = !pf5_;
                        break;
                      default:
                        break;
                    }
                    break;
                  default:
                    break;
                }
                break;
              case 'O':
                switch (b[i++]) {
                  case 'P': /* \eOP is F1 */
                    pf1_ = !pf1_;
                    break;
                  case 'Q': /* \eOQ is F2 */
                    pf2_ = !pf2_;
                    break;
                  case 'R': /* \eOR is F3 */
                    pf3_ = !pf3_;
                    break;
                  case 'S': /* \eOS is F4 */
                    pf4_ = !pf4_;
                    break;
                  case 'T': /* \eOT is F5 */
                    pf5_ = !pf5_;
                    break;
                  case 'U': /* \eOU is F6 */
                    pf6_ = !pf6_;
                    break;
                  case 'V': /* \eOV is F7 */
                    pf7_ = !pf7_;
                    break;
                  case 'W': /* \eOW is F8 */
                    pf8_ = !pf8_;
                    break;
                  case 'Y': /* \eOY is F10 */
                    pf10_ = !pf10_;
                    break;
                  case 'Z': /* \eOZ is F11 */
                    pf11_ = !pf11_;
                    break;
                  case '[': /* \eO[ is F12 */
                    pf12_ = !pf12_;
                    break;
                  default:
                    break;
                }
                break;
              default:
                break;
            }
            break;
          default:
            break;
        }
      }
      if ((n -= i) <= 0) {
        break;
      } else {
        memmove(b, b + i, sizeof(b) - i);
      }
    }
  } else if (errno == EINTR) {
    longjmp(jbi_, 1);
  }
}

static void PerformBestEffortIo(void) {
  int toto, pollms;
  struct pollfd fds[] = {
      {infd_, POLLIN},
      {outfd_, f1_ && f1_->n ? POLLOUT : 0},
  };
  pollms = MAX(0, timespec_tomillis(GetGraceTime()));
  DEBUGF("poll() ms=%,d", pollms);
  if ((toto = poll(fds, ARRAYLEN(fds), pollms)) != -1) {
    DEBUGF("poll() toto=%d [grace=%,ldns]", toto,
           timespec_tonanos(GetGraceTime()));
    if (toto) {
      if (fds[0].revents & (POLLIN | POLLHUP | POLLERR))
        ReadKeyboard();
      if (fds[1].revents & (POLLOUT | POLLHUP | POLLERR))
        WriteVideo();
    }
  } else if (errno == EINTR) {
    DEBUGF("poll() → EINTR");
    return;
  } else {
    FATALF("poll() → %s", strerror(errno));
  }
}

static void RestoreTty(void) {
  DrainVideo();
  if (ttymode_)
    ttysend(outfd_, "\r\n\e[J");
  ttymode_ = false;
  ttyraw(-1);
}

static void HandleSignals(void) {
  if (resized_)
    RefreshDisplay();
}

static void PrintVideo(void) {
  struct timespec decode_last, decode_end, next_tick, lag;
  dura_ = timespec_frommicros(min(MAX_FRAMERATE, 1 / plm_get_framerate(plm_)) *
                              1e6);
  INFOF("framerate=%f dura=%f", plm_get_framerate(plm_), dura_);
  next_tick = deadline_ = decode_last = timespec_mono();
  next_tick = timespec_add(next_tick, dura_);
  deadline_ = timespec_add(deadline_, dura_);
  do {
    DEBUGF("plm_decode [grace=%,ldns]", timespec_tonanos(GetGraceTime()));
    decode_start_ = timespec_mono();
    plm_decode(plm_,
               timespec_tofloat(timespec_sub(decode_start_, decode_last)));
    decode_last = decode_start_;
    decode_end = timespec_mono();
    lag = timespec_sub(decode_end, decode_start_);
    while (timespec_cmp(timespec_add(decode_end, lag), next_tick) > 0)
      next_tick = timespec_add(next_tick, dura_);
    deadline_ = timespec_sub(next_tick, lag);
    if (gotvideo_ || !plm_get_video_enabled(plm_)) {
      gotvideo_ = false;
      INFOF("entering printvideo event loop (lag=%,ldns, grace=%,ldns)",
            timespec_tonanos(lag), timespec_tonanos(GetGraceTime()));
    }
    do {
      if (!setjmp(jbi_))
        PerformBestEffortIo();
      HandleSignals();
    } while (timespec_tomillis(GetGraceTime()) > 0);
  } while (plm_ && !plm_has_ended(plm_));
}

static bool AskUserYesOrNoQuestion(const char *prompt) {
  char c;
  if (yes_ || !ttymode_)
    return true;
  ttysend(outfd_, "\r\e[K");
  ttysend(outfd_, prompt);
  ttysend(outfd_, " [yn] ");
  poll((struct pollfd[]){{infd_, POLLIN}}, 1, -1);
  c = 0, read(infd_, &c, 1);
  ttysend(infd_, "\r\e[K");
  return c == 'y' || c == 'Y';
}

static void PrintUsage(int rc, int fd) {
  tinyprint(fd, "Usage: ", program_invocation_name, USAGE, NULL);
  exit(rc);
}

static void GetOpts(int argc, char *argv[]) {
  int opt;
  snprintf(logpath_, sizeof(logpath_), "%s%s.log", __get_tmpdir(),
           firstnonnull(program_invocation_short_name, "unknown"));
  while ((opt = getopt(argc, argv, "?34AGSTVYabdfhnpstxyzvL:")) != -1) {
    switch (opt) {
      case 'y':
        yes_ = true;
        break;
      case 'v':
        ++__log_level;
        break;
      case 'L':
        snprintf(logpath_, sizeof(logpath_), "%s", optarg);
        break;
      case 'Y':
        yonly_ = true;
        break;
      case 'h':
      case '?':
      default:
        if (!ProcessOptKey(opt)) {
          if (opt == optopt) {
            PrintUsage(EXIT_SUCCESS, STDOUT_FILENO);
          } else {
            PrintUsage(EX_USAGE, STDERR_FILENO);
          }
        }
    }
  }
}

static void OnExit(void) {
  if (plm_)
    plm_destroy(plm_), plm_ = NULL;
  YCbCrFree(&ycbcr_);
  RestoreTty();
  ttyidentclear(&ti_);
  close(infd_), infd_ = -1;
  close(outfd_), outfd_ = -1;
  free(graphic_[0].b);
  free(graphic_[1].b);
  free(vtframe_[0].b);
  free(vtframe_[1].b);
  free(xtcodes_);
  free(audio_);
  CloseSpeaker();
}

static void PickDefaults(void) {
  /*
   * Direct color ain't true color -- it just means xterm does the
   * xterm256 rgb quantization for you. we're better at xterm256
   * than xterm is, so we don't need the training wheels.
   *
   * strcmp(nulltoempty(getenv("TERM")), "xterm-direct") == 0
   */
  if (IsWindows() || !strcmp(nulltoempty(getenv("TERM")), "xterm-kitty"))
    ttyquantsetup(kTtyQuantTrue, TTYQUANT()->chans, kTtyBlocksUnicode);
}

#define FBIOGET_VSCREENINFO 0x4600
#define FBIOGET_FSCREENINFO 0x4602

static void TryToOpenFrameBuffer(void) {
  /*
   * Linux (i.e. without some X or Wayland thing running on top of it)
   * is barely able to display any non-ascii characters, so things look
   * much better if we can access the framebuffer.
   */
  int rc;
  fb0_.fd = -1;
  fb0_.path = NULL;
  if (!isempty(getenv("FRAMEBUFFER"))) {
    fb0_.path = strdup(getenv("FRAMEBUFFER"));
  } else if (strcmp(nulltoempty(getenv("TERM")), "linux") == 0) {
    fb0_.path = strdup("/dev/fb0");
  }
  if ((fb0_.fd = open(fb0_.path, O_RDWR)) != -1) {
    CHECK_NE(-1, (rc = ioctl(fb0_.fd, FBIOGET_FSCREENINFO, &fb0_.fscreen)));
    INFOF("ioctl(%s) → %d", "FBIOGET_FSCREENINFO", rc);
    INFOF("%s.%s=%.*s", "fb0_.fscreen", "id", sizeof(fb0_.fscreen.id),
          fb0_.fscreen.id);
    INFOF("%s.%s=%p", "fb0_.fscreen", "smem_start", fb0_.fscreen.smem_start);
    INFOF("%s.%s=%u", "fb0_.fscreen", "smem_len", fb0_.fscreen.smem_len);
    INFOF("%s.%s=%u", "fb0_.fscreen", "type", fb0_.fscreen.type);
    INFOF("%s.%s=%u", "fb0_.fscreen", "type_aux", fb0_.fscreen.type_aux);
    INFOF("%s.%s=%u", "fb0_.fscreen", "visual", fb0_.fscreen.visual);
    INFOF("%s.%s=%hu", "fb0_.fscreen", "xpanstep", fb0_.fscreen.xpanstep);
    INFOF("%s.%s=%hu", "fb0_.fscreen", "ypanstep", fb0_.fscreen.ypanstep);
    INFOF("%s.%s=%hu", "fb0_.fscreen", "ywrapstep", fb0_.fscreen.ywrapstep);
    INFOF("%s.%s=%u", "fb0_.fscreen", "line_length", fb0_.fscreen.line_length);
    INFOF("%s.%s=%p", "fb0_.fscreen", "mmio_start", fb0_.fscreen.mmio_start);
    INFOF("%s.%s=%u", "fb0_.fscreen", "mmio_len", fb0_.fscreen.mmio_len);
    INFOF("%s.%s=%u", "fb0_.fscreen", "accel", fb0_.fscreen.accel);
    INFOF("%s.%s=%#b", "fb0_.fscreen", "capabilities",
          fb0_.fscreen.capabilities);
    CHECK_NE(-1, (rc = ioctl(fb0_.fd, FBIOGET_VSCREENINFO, &fb0_.vscreen)));
    INFOF("ioctl(%s) → %d", "FBIOGET_VSCREENINFO", rc);
    INFOF("%s.%s=%u", "fb0_.vscreen", "xres", fb0_.vscreen.xres);
    INFOF("%s.%s=%u", "fb0_.vscreen", "yres", fb0_.vscreen.yres);
    INFOF("%s.%s=%u", "fb0_.vscreen", "xres_virtual",
          fb0_.vscreen.xres_virtual);
    INFOF("%s.%s=%u", "fb0_.vscreen", "yres_virtual",
          fb0_.vscreen.yres_virtual);
    INFOF("%s.%s=%u", "fb0_.vscreen", "xoffset", fb0_.vscreen.xoffset);
    INFOF("%s.%s=%u", "fb0_.vscreen", "yoffset", fb0_.vscreen.yoffset);
    INFOF("%s.%s=%u", "fb0_.vscreen", "bits_per_pixel",
          fb0_.vscreen.bits_per_pixel);
    INFOF("%s.%s=%u", "fb0_.vscreen", "grayscale", fb0_.vscreen.grayscale);
    INFOF("%s.%s=%u", "fb0_.vscreen.red", "offset", fb0_.vscreen.red.offset);
    INFOF("%s.%s=%u", "fb0_.vscreen.red", "length", fb0_.vscreen.red.length);
    INFOF("%s.%s=%u", "fb0_.vscreen.red", "msb_right",
          fb0_.vscreen.red.msb_right);
    INFOF("%s.%s=%u", "fb0_.vscreen.green", "offset",
          fb0_.vscreen.green.offset);
    INFOF("%s.%s=%u", "fb0_.vscreen.green", "length",
          fb0_.vscreen.green.length);
    INFOF("%s.%s=%u", "fb0_.vscreen.green", "msb_right",
          fb0_.vscreen.green.msb_right);
    INFOF("%s.%s=%u", "fb0_.vscreen.blue", "offset", fb0_.vscreen.blue.offset);
    INFOF("%s.%s=%u", "fb0_.vscreen.blue", "length", fb0_.vscreen.blue.length);
    INFOF("%s.%s=%u", "fb0_.vscreen.blue", "msb_right",
          fb0_.vscreen.blue.msb_right);
    INFOF("%s.%s=%u", "fb0_.vscreen.transp", "offset",
          fb0_.vscreen.transp.offset);
    INFOF("%s.%s=%u", "fb0_.vscreen.transp", "length",
          fb0_.vscreen.transp.length);
    INFOF("%s.%s=%u", "fb0_.vscreen.transp", "msb_right",
          fb0_.vscreen.transp.msb_right);
    INFOF("%s.%s=%u", "fb0_.vscreen", "nonstd", fb0_.vscreen.nonstd);
    INFOF("%s.%s=%u", "fb0_.vscreen", "activate", fb0_.vscreen.activate);
    INFOF("%s.%s=%u", "fb0_.vscreen", "height", fb0_.vscreen.height);
    INFOF("%s.%s=%u", "fb0_.vscreen", "width", fb0_.vscreen.width);
    INFOF("%s.%s=%u", "fb0_.vscreen", "accel_flags", fb0_.vscreen.accel_flags);
    INFOF("%s.%s=%u", "fb0_.vscreen", "pixclock", fb0_.vscreen.pixclock);
    INFOF("%s.%s=%u", "fb0_.vscreen", "left_margin", fb0_.vscreen.left_margin);
    INFOF("%s.%s=%u", "fb0_.vscreen", "right_margin",
          fb0_.vscreen.right_margin);
    INFOF("%s.%s=%u", "fb0_.vscreen", "upper_margin",
          fb0_.vscreen.upper_margin);
    INFOF("%s.%s=%u", "fb0_.vscreen", "lower_margin",
          fb0_.vscreen.lower_margin);
    INFOF("%s.%s=%u", "fb0_.vscreen", "hsync_len", fb0_.vscreen.hsync_len);
    INFOF("%s.%s=%u", "fb0_.vscreen", "vsync_len", fb0_.vscreen.vsync_len);
    INFOF("%s.%s=%u", "fb0_.vscreen", "sync", fb0_.vscreen.sync);
    INFOF("%s.%s=%u", "fb0_.vscreen", "vmode", fb0_.vscreen.vmode);
    INFOF("%s.%s=%u", "fb0_.vscreen", "rotate", fb0_.vscreen.rotate);
    INFOF("%s.%s=%u", "fb0_.vscreen", "colorspace", fb0_.vscreen.colorspace);
    fb0_.size = fb0_.fscreen.smem_len;
    CHECK_NE(MAP_FAILED,
             (fb0_.map = mmap(NULL, fb0_.size, PROT_READ | PROT_WRITE,
                              MAP_SHARED, fb0_.fd, 0)));
  }
}

bool IsCharDev(int fd) {
  int e = errno;
  struct stat st;
  if (!fstat(fd, &st))
    return S_ISCHR(st.st_mode);
  errno = e;
  return false;
}

int main(int argc, char *argv[]) {
  sigset_t wut;
  ShowCrashReports();
#ifdef __x86_64__
  if (IsWindows())
    SetThreadPriority(GetCurrentThread(), kNtThreadPriorityHighest);
#endif
  gamma_ = 2.4;
  volscale_ = 1.f;
  dither_ = true;
  sigemptyset(&wut);
  sigaddset(&wut, SIGCHLD);
  sigaddset(&wut, SIGPIPE);
  sigprocmask(SIG_SETMASK, &wut, NULL);
  ShowCrashReports();
  fullclear_ = true;
  GetOpts(argc, argv);
  if (!tuned_)
    PickDefaults();
  if (optind == argc)
    PrintUsage(EX_USAGE, STDERR_FILENO);
  patharg_ = argv[optind];
  infd_ = STDIN_FILENO;
  outfd_ = STDOUT_FILENO;
  if (!setjmp(jb_)) {
    xsigaction(SIGINT, OnCtrlC, 0, 0, NULL);
    xsigaction(SIGHUP, OnCtrlC, 0, 0, NULL);
    xsigaction(SIGTERM, OnCtrlC, 0, 0, NULL);
    xsigaction(SIGWINCH, OnResize, 0, 0, NULL);
    if (ttyraw(kTtyLfToCrLf) != -1)
      ttymode_ = true;
    __cxa_atexit((void *)OnExit, NULL, NULL);
    __log_file = fopen(logpath_, "a");
    if (IsCharDev(infd_) && IsCharDev(outfd_)) {
      /* CHECK_NE(-1, fcntl(outfd_, F_SETFL, O_NONBLOCK)); */
    } else if (infd_ != outfd_) {
      infd_ = -1;
    }
    /* CHECK_NE(-1, fcntl(outfd_, F_SETFL, O_NONBLOCK)); */
    TryToOpenFrameBuffer();
    if (t2 > t1)
      longjmp(jb_, 1);
    OpenVideo();
    DimensionDisplay();
    starttime_ = timespec_mono();
    PrintVideo();
  }
  INFOF("jb_ triggered");
  return 0;
}
